/**
 * Copyright (c) The openTCS Authors.
 * <p>
 * This program is free software and subject to the MIT license. (For details,
 * see the licensing information (LICENSE.txt) you should have received with
 * this copy of the software.)
 */
package org.opentcs.strategies.basic.scheduling;

import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;

import java.util.*;

import static java.util.Objects.requireNonNull;

import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import org.opentcs.components.kernel.Scheduler;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.DyRerouteEvent;
import org.opentcs.data.MsgShowEvent;
import org.opentcs.data.TCSObject;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.TCSResource;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.util.event.EventBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 */
public class ReservationPool {

  /**
   * This class's Logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(ReservationPool.class);
  /**
   * All claims.
   */
  private final Map<Scheduler.Client, Queue<Set<TCSResource<?>>>> claimsByClient
      = new HashMap<>();
  /**
   * <code>ReservationEntry</code> instances for each <code>TCSResource</code>.
   */
  private final Map<TCSResource<?>, ReservationEntry> reservations = new HashMap<>();

  private Map<String, BlockMessage> blockMessageMap = new HashMap<>();
  /**
   * 形成了环的小车名字的集合
   */
  private Set<BlockMessage> ringBlocks = new HashSet<>();

  private final EventBus eventBus;

  /**
   * Creates a new instance.
   */
  @Inject
  public ReservationPool(@ApplicationEventBus EventBus eventBus) {
    this.eventBus = requireNonNull(eventBus);
  }

  /**
   * Returns a reservation entry for the given resource.
   *
   * @param resource The resource for which to return the reservation entry.
   * @return The reservation entry for the given resource.
   */
  @Nonnull
  public ReservationEntry getReservationEntry(TCSResource<?> resource) {
    requireNonNull(resource, "resource");

    ReservationEntry entry = reservations.get(resource);
    if (entry == null) {
      entry = new ReservationEntry(resource);
      reservations.put(resource, entry);
    }
    return entry;
  }

  /**
   * Returns the sequence of resource sets claimed by the given client.
   *
   * @param client The client.
   * @return The sequence of resource sets claimed by the given client.
   */
  @Nonnull
  public List<Set<TCSResource<?>>> getClaim(@Nonnull Scheduler.Client client) {
    requireNonNull(client, "client");

    return claimsByClient.getOrDefault(client, new ArrayDeque<>()).stream()
        .map(resourceSet -> Set.copyOf(resourceSet))
        .collect(Collectors.toList());
  }

  /**
   * Sets the sequence of claimed resource sets for the given client.
   *
   * @param client    The client.
   * @param resources The sequence of claimed resources.
   */
  public void setClaim(@Nonnull Scheduler.Client client,
                       @Nonnull List<Set<TCSResource<?>>> resources) {
    requireNonNull(client, "client");
    requireNonNull(resources, "resources");

    claimsByClient.put(client, new ArrayDeque<>(resources));
  }

  /**
   * Removes the given resource set from the head of the sequence of claimed resource sets for the
   * given client.
   *
   * @param client    The client.
   * @param resources The resource set to be removed from the head of the client's claim sequence.
   * @throws IllegalArgumentException If the given resource set is not the head of the client's
   *                                  claim sequence.
   */
  public void unclaim(@Nonnull Scheduler.Client client, @Nonnull Set<TCSResource<?>> resources)
      throws IllegalArgumentException {
    requireNonNull(client, "client");
    requireNonNull(resources, "resources");

    if (!claimsByClient.containsKey(client) || claimsByClient.get(client).isEmpty()) {
      return;
    }
    //分配成功之后，需要将block信息进行清除一下
    removeBlockMessage(client.getId());

    if (!isNextInClaim(client, resources)) {
      throw new IllegalArgumentException(
          String.format("Resources to unclaim and head of claimed resource don't match: %s != %s",
              resources,
              claimsByClient.get(client).peek())
      );
    }

    Set<TCSResource<?>> remove = claimsByClient.get(client).remove();
    LOG.info("{} unclaimed {} resources", client, Arrays.toString(remove.toArray()));
  }

  /**
   * Checks whether the given resource set is at the head of the given client's claim sequence.
   *
   * @param client    The client.
   * @param resources The resources to be checked.
   * @return <code>true</code> if, and only if, the given resource set is at the head of the given
   * client's claim sequence.
   */
  public boolean isNextInClaim(@Nonnull Scheduler.Client client,
                               @Nonnull Set<TCSResource<?>> resources) {
    requireNonNull(client, "client");
    requireNonNull(resources, "resources");

    if (!claimsByClient.containsKey(client) || claimsByClient.get(client).isEmpty()) {
      return false;
    }

    if (!Objects.equals(resources, claimsByClient.get(client).peek())) {
      return false;
    }

    return true;
  }

  /**
   * Returns all resources allocated by the given client.
   *
   * @param client The client for which to return all allocated resources.
   * @return All resources allocated by the given client.
   */
  @Nonnull
  public Set<TCSResource<?>> allocatedResources(@Nonnull Scheduler.Client client) {
    requireNonNull(client, "client");

    return reservations.entrySet().stream()
        .filter(entry -> entry.getValue().isAllocatedBy(client))
        .map(entry -> entry.getKey())
        .collect(Collectors.toSet());
  }

  /**
   * Checks if all resources in the given set of resources are be available for the given client.
   *
   * @param resources The set of resources to be checked.
   * @param client    The client for which to check.
   * @return <code>true</code> if, and only if, all resources in the given set
   * are available for the given client.
   */
  public boolean resourcesAvailableForUser(@Nonnull Set<TCSResource<?>> resources,
                                           @Nonnull Scheduler.Client client) {
    requireNonNull(resources, "resources");
    requireNonNull(client, "client");

    for (TCSResource<?> curResource : resources) {
      // Check if the resource is available.
      ReservationEntry entry = getReservationEntry(curResource);
      if (!entry.isFree() && !entry.isAllocatedBy(client)) {
        LOG.debug("{}: Resource {} unavailable, reserved by {}",
            client.getId(),
            curResource.getName(),
            entry.getClient().getId());
        return false;
      }
    }
    return true;
  }

  /**
   * 用于重路由的时候，判断新分配的资源是否会造成环（简单假性判断）
   *
   * @param resources
   * @param clientId
   * @return
   */
  public boolean willCaseRingBlock(@Nonnull Set<TCSResource<?>> resources,
                                   @Nonnull String clientId) {
    requireNonNull(resources, "resources");
    requireNonNull(clientId, "client");

    String blockId = null;
    //判断是否会申请出环
    for (TCSResource<?> curResource : resources) {
      // Check if the resource is available.
      ReservationEntry entry = getReservationEntry(curResource);
      if (!entry.isFree()) {
        blockId = entry.getClient().getId();
        break;
      }
    }
    Set<String> blockIds = new HashSet<>();
    while (blockId != null) {
      BlockMessage blockMessage = blockMessageMap.get(blockId);
      if (blockMessage == null)
        return false;
      blockId = blockMessage.getCaseBlockVehicleName();
      if (blockIds.contains(blockId)) {
        LOG.info("已经存在环了，重路由不进行约束");
        break;
      }
      blockIds.add(blockId);
      if (Objects.equals(blockId, clientId))
        return true;
    }
    return false;
  }

  /**
   * 该方法是一个resourcesAvailableForUserd方法的增强版本，增强了环问题的控制逻辑
   *
   * @param resources
   * @param client
   * @return
   */
  public boolean resourcesAvailableForUserWithBlock(@Nonnull Set<TCSResource<?>> resources,
                                                    @Nonnull Scheduler.Client client) {
    requireNonNull(resources, "resources");
    requireNonNull(client, "client");

    for (TCSResource<?> curResource : resources) {
      // Check if the resource is available.
      ReservationEntry entry = getReservationEntry(curResource);
      if (!entry.isFree() && !entry.isAllocatedBy(client)) {
        Scheduler.Client allocateClient = entry.getClient();
        String clientId = allocateClient.getId();
        //判断当前阻塞的资源是否是因为还未及时释放导致的,如果已经申请的资源小于3个说明小车没有可释放的资源了（未考虑车厂暂用的情况）
//        if (getUsedMap(clientId).size() < 3) {
//          blockMessageMap.put(client.getId(), new BlockMessage(client.getId(), clientId, resources.stream().map(TCSObject::getName).collect(Collectors.toSet())));
//        } else {
//          Set<ReservationEntry> usedMap = getUsedMap(client.getId());
//          System.out.println();
//        }
        blockMessageMap.put(client.getId(), new BlockMessage(client.getId(), clientId, Set.copyOf(resources)));
        return false;
      }
    }
    return true;
  }


  public boolean isCaseRing(Scheduler.Client client) {
    BlockMessage blockMessage = blockMessageMap.get(client.getId());
    List<BlockMessage> blockVehicleNames = new ArrayList<>();
    while (blockMessage != null) {
      blockVehicleNames.add(blockMessage);
      String caseBlockVehicleName = blockMessage.getCaseBlockVehicleName();
      BlockMessage blockMessage1 = blockMessageMap.get(caseBlockVehicleName);
      if (!blockMessage.isLinkBlock(blockMessage1))
        break;
      if ((blockMessage1.getBlockVehicleName().equals(client.getId()))) {
        blockVehicleNames.add(blockMessage1);
        //发布一个事件进行重路由
        if (blockVehicleNames.size() != ringBlocks.size() || !ringBlocks.containsAll(blockVehicleNames)) {
          cleanRingBLocks();
          ringBlocks.addAll(blockVehicleNames);
          LOG.warn("{} 分配资源导致出现环状态,环：{}", client.getId(), Arrays.toString(blockVehicleNames.stream().map(BlockMessage::getBlockVehicleName).toArray()));
          return true;
        }
        return false;
      } else if (blockVehicleNames.contains(blockMessage1)) {
        LOG.warn("{} 之前分配已经出现环状态,环：{}", client.getId(), Arrays.toString(blockVehicleNames.stream().map(BlockMessage::getBlockVehicleName).toArray()));
        return false;
      } else
        blockMessage = blockMessage1;
    }
    return false;
  }

  /**
   * 发布动态路由时间方法
   */
  public void pushDyEvent()
  {
    eventBus.onEvent(new DyRerouteEvent());
  }

  /**
   * 发布堵塞信息时间，给前端插件进行展示
   * @param currentVehicleName
   */
  public void pushBlockEvent(String currentVehicleName) {
    List<String> blockLinkMsg = getBlockLinkMsg(currentVehicleName);
    eventBus.onEvent(new MsgShowEvent(currentVehicleName, blockLinkMsg));
  }

  /***
   * 获取造成指定车辆堵塞的堵塞车辆链条信息
   * @param rootVehicleName
   * @return
   */
  public List<String> getBlockLinkMsg(String rootVehicleName) {
    List<String> result = new ArrayList<>();
    BlockMessage blockMessage = blockMessageMap.get(rootVehicleName);
    while (blockMessage != null) {
      String caseBlockVehicleName = blockMessage.getCaseBlockVehicleName();
      //避免出现环的时候，出现死循环
      if (Objects.equals(caseBlockVehicleName, rootVehicleName) || result.contains(caseBlockVehicleName))
      {
        result.add(caseBlockVehicleName);
        result.add("出现了环");
        break;
      }
      result.add(caseBlockVehicleName);
      blockMessage = blockMessageMap.get(caseBlockVehicleName);
    }
    return result;
  }


  public void addMsgShow(String clientId, String caseBlockVehicleName, Set<TCSResource<?>> resources, boolean caseVehicleIsInBlock) {
    BlockMessage blockMessage = new BlockMessage(clientId, caseBlockVehicleName, Set.copyOf(resources));
    blockMessage.setInBlock(true);
    blockMessageMap.put(clientId, blockMessage);
  }

  public void cleanRingBLocks() {
    ringBlocks.clear();
  }

  public void free(@Nonnull Scheduler.Client client, @Nonnull Set<TCSResource<?>> resources) {
    requireNonNull(client, "client");
    requireNonNull(resources, "resources");

    LOG.debug("{}: Releasing resources: {}", client.getId(), resources);
    for (TCSResource<?> curResource : getFreeableResources(resources, client)) {
      getReservationEntry(curResource).free();
    }
  }

  public void freeAll(@Nonnull Scheduler.Client client) {
    requireNonNull(client, "client");

    reservations.values().stream()
        .filter(reservationEntry -> reservationEntry.isAllocatedBy(client))
        .forEach(reservationEntry -> reservationEntry.freeCompletely());
    blockMessageMap.clear();
    ringBlocks.clear();
  }

  public void removeBlockMessage(String clientId) {
    blockMessageMap.remove(clientId);
    Set<String> collect = blockMessageMap.values().stream().filter(blockMessageVar -> blockMessageVar.caseBlockVehicleName.equals(clientId))
        .map(BlockMessage::getBlockVehicleName)
        .collect(Collectors.toSet());
    for (String s : collect) {
      BlockMessage remove = blockMessageMap.remove(s);
      LOG.info("因为{}分配成功释放资源，解除{} block信息:{}", clientId, s, remove.toString());
    }
  }

  @Nonnull
  public Map<String, Set<TCSResource<?>>> getAllocations() {
    final Map<String, Set<TCSResource<?>>> result = new HashMap<>();
    for (Map.Entry<TCSResource<?>, ReservationEntry> curEntry : reservations.entrySet()) {
      final TCSResource<?> curResource = curEntry.getKey();
      final Scheduler.Client curUser = curEntry.getValue().getClient();
      if (curUser != null) {
        Set<TCSResource<?>> userResources = result.get(curUser.getId());
        if (userResources == null) {
          userResources = new HashSet<>();
        }
        userResources.add(curResource);
        result.put(curUser.getId(), userResources);
      }
    }
    return result;
  }

  public void clear() {
    claimsByClient.clear();
    reservations.clear();
    blockMessageMap.clear();
    ringBlocks.clear();
  }

  /**
   * Returns a set of resources that is a subset of the given set of resources and is reserved/could
   * be released by the given client.
   *
   * @param resources The set of resources to be filtered for resources that could be released.
   * @param client    The client that should be able to release the returned resources.
   * @return A set of resources that is a subset of the given set of resources and is reserved/could
   * be released by the given client.
   */
  @Nonnull
  private Set<TCSResource<?>> getFreeableResources(@Nonnull Set<TCSResource<?>> resources,
                                                   @Nonnull Scheduler.Client client) {
    // Make sure we're freeing only resources that are allocated by us.
    final Set<TCSResource<?>> freeableResources = new HashSet<>();
    for (TCSResource<?> curRes : resources) {
      ReservationEntry entry = getReservationEntry(curRes);
      if (!entry.isAllocatedBy(client)) {
        LOG.warn("{}: Freed resource not reserved: {}, entry: {}", client.getId(), curRes, entry);
      } else {
        freeableResources.add(curRes);
      }
    }
    return freeableResources;
  }

  /**
   * 获取一个对象已经分配好了的资源
   *
   * @return
   */
  public Set<ReservationEntry> getUsedMap(String clientName) {
    return reservations.values().stream().filter(source -> !source.isFree() && source.getClient().getId().equalsIgnoreCase(clientName)).collect(Collectors.toSet());
  }

  /**
   * 获取当前小车所有已经被分配的资源,这里是将手机进行多出
   *
   * @param clientName
   * @return
   */
  public Set<TCSResourceReference<?>> getClaimsMap(String clientName) {
    Set<TCSResourceReference<?>> costMap = new HashSet<>();
    for (Scheduler.Client client : claimsByClient.keySet()) {
      if (client.getId().equalsIgnoreCase(clientName))
        continue;
      Queue<Set<TCSResource<?>>> sets = claimsByClient.get(client);
      sets.forEach(set -> costMap.addAll(set.stream().map(TCSResource::getReference).collect(Collectors.toSet())));
    }
    return costMap;
  }

  public Map<String, Integer> getClaimsMaps(String clientName) {
    HashMap<String, Integer> resultMap = new HashMap<>();
    for (Scheduler.Client client : claimsByClient.keySet()) {
      if (client.getId().equalsIgnoreCase(clientName))
        continue;
      Queue<Set<TCSResource<?>>> sets = claimsByClient.get(client);
      sets.forEach(set -> set.forEach(setVar -> {
            resultMap.put(setVar.getName(), resultMap.getOrDefault(setVar.getName(), 0) + 1);
          })
      );
    }
    return resultMap;
  }

  public Set<String> getBlockVehicleNames() {
    //过滤那些正在block当中的小车。
    Set<String> collect = ringBlocks.stream().filter(blockVar -> blockVar.isInBlock).map(BlockMessage::getCaseBlockVehicleName).collect(Collectors.toSet());
    return ringBlocks.stream().map(BlockMessage::getBlockVehicleName).filter(blockVehicleName -> !collect.contains(blockVehicleName)).collect(Collectors.toSet());
  }

  /**
   * 获取环堵塞的时候，指定车辆堵塞的资源。
   *
   * @param vehicleName
   * @return
   */
  public Set<String> getBlockSourceByVehicleName(String vehicleName) {
    BlockMessage blockMessage = blockMessageMap.get(vehicleName);
    if (blockMessage == null)
      return Set.of();
    return blockMessage.getBlockResources().stream().map(TCSObject::getName).collect(Collectors.toSet());
  }

  @Data
  @ToString
  public static class BlockMessage {
    /**
     * 当前阻塞的车的名字
     */
    private String blockVehicleName;
    /**
     * 导致当前阻塞的车的车名字
     */
    private String caseBlockVehicleName;
    /**
     * 堵塞的资源：用于后面进行动态重路由的时候一个加权值。
     */
    Set<TCSResource<?>> blockResources;

    private boolean isInBlock = false;

    public BlockMessage(String blockVehicleName, String caseBlockVehicleName, Set<TCSResource<?>> blockResources) {
      this.blockVehicleName = blockVehicleName;
      this.blockResources = blockResources;
      this.caseBlockVehicleName = caseBlockVehicleName;
    }


    public boolean isLinkBlock(BlockMessage other) {
      if (other == null)
        return false;
      //如果名字都不想同那么就直接报错
      if (!this.caseBlockVehicleName.equals(other.getBlockVehicleName()))
        return false;
      //如果当前堵塞的资源是在block当中，那么只要。
      if (this.isInBlock)
        return true;
      Path path1 = this.getBlockPathName();
      Path path2 = other.getBlockPathName();
//      如果当前的终点正好是other的起点那么就是连在一起的堵塞
      String name = path1.getDestinationPoint().getName();
      String name1 = path2.getSourcePoint().getName();
      return name.equals(name1);

    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof BlockMessage)) return false;
      BlockMessage that = (BlockMessage) o;
      return isInBlock == that.isInBlock && Objects.equals(blockVehicleName, that.blockVehicleName) && Objects.equals(caseBlockVehicleName, that.caseBlockVehicleName) && Objects.equals(blockResources, that.blockResources);
    }

    @Override
    public int hashCode() {
      return Objects.hash(blockVehicleName, caseBlockVehicleName, blockResources, isInBlock);
    }

    public Path getBlockPathName() {
      return (Path) this.blockResources.stream().filter(var -> var instanceof Path).findFirst().orElse(null);
    }

  }
}
